"""
@author: 江同学呀
@file: joining_queries.py
@date: 2025/1/13 20:37
@desc:
    联接查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/joining-queries.html

    Performing full SQL-style joins in a distributed system like Elasticsearch is prohibitively expensive. Instead,
    Elasticsearch offers two forms of join which are designed to scale horizontally.
    在 Elasticsearch 这样的分布式系统中执行完整的 SQL 样式连接成本高得令人望而却步。相反，Elasticsearch 提供了两种形式的联接，它们旨在水平扩展。
"""
from typing import Union, Literal, Dict, List, Iterable

from espc.common.query_common import ScoreMode
from espc.orm.model.dsl.inner_hits_options import InnerHits
from espc.orm.model.dsl.queries.base_queries import _BaseQueries
from espc.orm.model.mapping.field.base_field.base_field import _BaseField
from espc.orm.model.mapping.field.object.nested_field import NestedField


class Nested(_BaseQueries):
    """
    嵌套查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-nested-query.html

    Wraps another query to search nested fields.
    包装另一个查询以搜索嵌套字段。

    The nested query searches nested field objects as if they were indexed as separate documents. If an object matches
    the search, the nested query returns the root parent document.
    嵌套查询搜索嵌套字段对象，就像它们作为单独的文档编制索引一样。如果对象与搜索匹配，则嵌套查询将返回根父文档。

    :param path:
        (Required, string) Path to the nested object you wish to search.
        （必需，字符串）要搜索的嵌套对象的路径。
    :param query:
        (Required, query object) Query you wish to run on nested objects in the path. If an object matches the search,
        the nested query returns the root parent document.
        （必需，查询对象）您希望对路径中的嵌套对象运行的查询。如果对象与搜索匹配，则嵌套查询将返回根父文档。

        You can search nested fields using dot notation that includes the complete path, such as obj1.name.
        您可以使用包含完整路径的点表示法（如 obj1.name）搜索嵌套字段。

        Multi-level nesting is automatically supported, and detected, resulting in an inner nested query to
        automatically match the relevant nesting level, rather than root, if it exists within another nested query.
        自动支持并检测多级嵌套，从而使内部嵌套查询自动匹配相关的嵌套级别，而不是根嵌套级别（如果它存在于另一个嵌套查询中）。

        See Multi-level nested queries for an example.
        有关示例，请参阅多级嵌套查询。
    :param score_mode:
        (Optional, string) Indicates how scores for matching child objects affect the root parent document’s relevance
        score. Valid values are:
        avg (Default)
        Use the mean relevance score of all matching child objects.
        max
        Uses the highest relevance score of all matching child objects.
        使用所有匹配子对象的最高相关性分数。
        min
        Uses the lowest relevance score of all matching child objects.
        使用所有匹配子对象的最低相关性分数。
        none
        Do not use the relevance scores of matching child objects. The query assigns parent documents a score of 0.
        请勿使用匹配子对象的相关性分数。查询为父文档分配分数 0。
        sum
        Add together the relevance scores of all matching child objects.
        将所有匹配子对象的相关性分数相加。

        （可选，字符串）指示匹配子对象的分数如何影响根父文档的相关性分数。有效值为：
            - avg （默认）
                使用所有匹配子对象的平均相关性分数。
            - max
                使用所有匹配子对象的最高相关性分数。
            - min
                使用所有匹配子对象的最低相关性分数。
            - none

                请勿使用匹配子对象的相关性分数。查询为父文档分配分数 0。
            - sum
                将所有匹配子对象的相关性分数相加。
    :param ignore_unmapped:
        (Optional, Boolean) Indicates whether to ignore an unmapped path and not return any documents instead of an
        error. Defaults to false.
        If false, Elasticsearch returns an error if the path is an unmapped field.
        You can use this parameter to query multiple indices that may not contain the field path.

        （可选，布尔值）指示是否忽略未映射的路径，并且不返回任何文档而不是错误。默认为 false。
        如果为 false，则如果路径是未映射的字段，则 Elasticsearch 将返回错误。
        您可以使用此参数查询可能不包含字段路径的多个索引。
    :param inner_hits:
    """
    type: str = "nested"

    def __init__(
            self, path: Union[str, NestedField], query: Union[Dict, _BaseQueries],
            score_mode: Union[Literal["avg", "max", "min", "none", "sum"], ScoreMode] = None,
            ignore_unmapped: bool = None,
            inner_hits: Union[
                InnerHits,
                Dict[
                    Literal["from", "size", "sort", "name", "collapse", "_source", "docvalue_fields"],
                    Union[int, bool, Dict, Iterable[Union[str, _BaseField]]]
                ]
            ] = None
    ):
        super().__init__()
        self._path: Union[str, NestedField] = path
        self._query: Union[Dict, _BaseQueries] = query
        self._score_mode: Union[Literal["avg", "max", "min", "none", "sum"], ScoreMode, None] = score_mode
        self._ignore_unmapped: bool = ignore_unmapped
        self._inner_hits: Union[
            InnerHits,
            Dict[
                Literal["from", "size", "sort", "name", "collapse", "_source", "docvalue_fields"],
                Union[int, bool, Dict, Iterable[Union[str, _BaseField]]]
            ]
        ] = inner_hits
        return

    def _build(self) -> Dict:
        """
        :return:
        """
        body = {
            "path": self._path if isinstance(self._path, str) else self._path._field_name_path,
            "query": self._query if isinstance(self._query, Dict) else {self._query.type: self._query._build()}
        }
        if self._score_mode:
            body["score_mode"] = self._score_mode if isinstance(self._score_mode, str) else self._score_mode.value
        if self._ignore_unmapped is not None:
            body["ignore_unmapped"] = self._ignore_unmapped
        if self._inner_hits:
            body["inner_hits"] = (
                self._inner_hits._build()
                if isinstance(self._inner_hits, InnerHits)
                else {
                    k:
                        v
                        if isinstance(v, (str, int, bool, Dict))
                        else [
                            field if isinstance(field, str) else field._field_name
                            for field in v
                        ]
                    for k, v in self._inner_hits.items()
                }
            )
        return body


class HasChild(_BaseQueries):
    """
    具有子查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-has-child-query.html

    Returns parent documents whose joined child documents match a provided query. You can create parent-child
    relationships between documents in the same index using a join field mapping.
    返回其联接的子文档与提供的查询匹配的父文档。您可以使用联接字段映射在同一索引中的文档之间创建父子关系。

    Because it performs a join, the has_child is slow compared to other queries. Its performance degrades as the number
    of matching child documents pointing to unique parent documents increases. Each has_child query in a search can
    increase query time significantly.
    由于它执行联接，因此与其他查询相比，has_child速度较慢。随着指向唯一父文档的匹配子文档数量的增加，其性能会下降。搜索中的每个has_child查询
    都会显著增加查询时间。

    If you care about query performance, do not use this query. If you need to use the has_child query, use it as
    rarely as possible.
    如果您关心查询性能，请不要使用此查询。如果您需要使用 has_child 查询，请尽可能少地使用它。

    :param type_:
        (Required, string) Name of the child relationship mapped for the join field.
        （必需，字符串）为联接字段映射的子关系的名称。
    :param query:
        (Required, query object) Query you wish to run on child documents of the type field. If a child document
        matches the search, the query returns the parent document.
        （必需，查询对象）您希望对 type 字段的子文档运行的查询。如果子文档与搜索匹配，则查询将返回父文档。
    :param ignore_unmapped:
        (Optional, Boolean) Indicates whether to ignore an unmapped type and not return any documents instead of an
        error. Defaults to false.
        If false, Elasticsearch returns an error if the type is unmapped.
        You can use this parameter to query multiple indices that may not contain the type.

        （可选，布尔值）指示是否忽略未映射的类型，并且不返回任何文档而不是错误。默认为 false。
        如果为 false，则 Elasticsearch 在类型未映射时返回错误。
        您可以使用此参数查询可能不包含该类型的多个索引。
    :param max_children:
        (Optional, integer) Maximum number of child documents that match the query allowed for a returned parent
        document. If the parent document exceeds this limit, it is excluded from the search results.
        （可选，整数）与返回的父文档允许的查询匹配的最大子文档数。如果父文档超过此限制，则会将其从搜索结果中排除。
    :param min_children:
        (Optional, integer) Minimum number of child documents that match the query required to match the query for a
        returned parent document. If the parent document does not meet this limit, it is excluded from the search
        results.
        （可选，整数）与返回的父文档的查询匹配的查询所需的最小子文档数。如果父文档不满足此限制，则会将其从搜索结果中排除。
    :param score_mode:
        (Optional, string) Indicates how scores for matching child documents affect the root parent document’s relevance
        score. Valid values are:
            - none (Default)
                Do not use the relevance scores of matching child documents. The query assigns parent documents a
                score of 0.
            - avg
                Use the mean relevance score of all matching child documents.
            - max
                Uses the highest relevance score of all matching child documents.
            - min
                Uses the lowest relevance score of all matching child documents.
            - sum
                Add together the relevance scores of all matching child documents.
        （可选，字符串）指示匹配子文档的分数如何影响根父文档的相关性分数。有效值为：
            - none （默认）
                请勿使用匹配子文档的相关性分数。查询为父文档分配分数 0。
            - avg
                使用所有匹配子文档的平均相关性分数。
            - max
                使用所有匹配的子文档的最高相关性分数。
            - min
                使用所有匹配的子文档的最低相关性分数。
            - sum
                将所有匹配子文档的相关性分数相加。
    """
    type: str = "has_child"

    def __init__(
            self, type_: str, query: Union[_BaseQueries, Dict], ignore_unmapped: bool = None, max_children: int = None,
            min_children: int = None, score_mode: Union[Literal["none", "avg", "max", "min", "sum"], ScoreMode] = None
    ):
        super().__init__()
        self._type: str = type_
        self._query: Union[_BaseQueries, Dict] = query
        self._ignore_unmapped: bool = ignore_unmapped
        self._max_children: int = max_children
        self._min_children: int = min_children
        self._score_mode: Union[Literal["none", "avg", "max", "min", "sum"], ScoreMode, None] = score_mode
        return

    def _build(self) -> Dict:
        """
        构建查询
        """
        _body: Dict = {
            "type": self._type,
            "query": self._query if isinstance(self._query, Dict) else {self._query.type: self._query._build()}
        }
        if self._ignore_unmapped is not None:
            _body["ignore_unmapped"] = self._ignore_unmapped
        if self._max_children is not None:
            _body["max_children"] = self._max_children
        if self._min_children is not None:
            _body["min_children"] = self._min_children
        if self._score_mode:
            _body["score_mode"] = self._score_mode if isinstance(self._score_mode, str) else self._score_mode.value
        return _body


class HasParent(_BaseQueries):
    """
    具有父查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-has-parent-query.html

    Returns child documents whose joined parent document matches a provided query. You can create parent-child
    relationships between documents in the same index using a join field mapping.
    返回其联接的父文档与提供的查询匹配的子文档。您可以使用联接字段映射在同一索引中的文档之间创建父子关系。

    Because it performs a join, the has_parent query is slow compared to other queries. Its performance degrades as the
    number of matching parent documents increases. Each has_parent query in a search can increase query time
    significantly.
    由于它执行联接，因此与其他查询相比，has_parent查询速度较慢。其性能会随着匹配父文档数量的增加而降低。搜索中的每个has_parent查询都会显著增加查询时间。

    :param parent_type:
        (Required, string) Name of the parent relationship mapped for the join field.
        （必需，字符串）为联接字段映射的父关系的名称。
    :param query:
        (Required, query object) Query you wish to run on parent documents of the parent_type field. If a parent
        document matches the search, the query returns its child documents.
        （必需，查询对象）您希望对 parent_type 字段的父文档运行的查询。如果父文档与搜索匹配，则查询将返回其子文档。
    :param score:
        (Optional, Boolean) Indicates whether the relevance score of a matching parent document is aggregated into its
        child documents. Defaults to false.
        If false, Elasticsearch ignores the relevance score of the parent document. Elasticsearch also assigns each
        child document a relevance score equal to the query's boost, which defaults to 1.
        If true, the relevance score of the matching parent document is aggregated into its child documents' relevance
        scores.

        （可选，布尔值）指示是否将匹配父文档的相关性分数聚合到其子文档中。默认为 false。
        如果为 false，则 Elasticsearch 将忽略父文档的相关性分数。Elasticsearch 还会为每个子文档分配一个等于查询提升的相关性分数，默认为 1。
        如果为 true，则匹配父文档的相关性分数将聚合到其子文档的相关性分数中。
    :param ignore_unmapped:
        (Optional, Boolean) Indicates whether to ignore an unmapped parent_type and not return any documents instead of
        an error. Defaults to false.
        If false, Elasticsearch returns an error if the parent_type is unmapped.
        You can use this parameter to query multiple indices that may not contain the parent_type.

        （可选，布尔值）指示是否忽略未映射的parent_type，并且不返回任何文档，而不是错误。默认为 false。
        如果为 false，则 Elasticsearch 将在 parent_type 未映射时返回错误。
        您可以使用此参数查询可能不包含 parent_type 的多个索引。
    """
    type: str = "has_parent"

    def __init__(
            self, parent_type: str, query: Union[_BaseQueries, Dict], score: bool = None, ignore_unmapped: bool = None,
    ):
        super().__init__()
        self._parent_type: str = parent_type
        self._query: Union[_BaseQueries, Dict] = query
        self._score: bool = score
        self._ignore_unmapped: bool = ignore_unmapped
        return

    def _build(self) -> Dict:
        """
        构建查询
        """
        _body: Dict = {
            "parent_type": self._parent_type,
            "query": self._query if isinstance(self._query, Dict) else {self._query.type: self._query._build()}
        }
        if self._score is not None:
            _body["score"] = self._score
        if self._ignore_unmapped is not None:
            _body["ignore_unmapped"] = self._ignore_unmapped
        return _body












